Skip to content

fix(cli): restore visible git fetch progress and defer autostash until update confirmed (#3523) - #62962

Open
sahilthakur456111-stack wants to merge 1 commit into
NousResearch:mainfrom
sahilthakur456111-stack:fix/3523-update-stash-and-fetch
Open

sahilthakur456111-stack wants to merge 1 commit into
NousResearch:mainfrom
sahilthakur456111-stack:fix/3523-update-stash-and-fetch

Conversation

@sahilthakur456111-stack

Copy link
Copy Markdown

Fixes #3523

Symptom

Regression from #3492 introduced two user-visible issues in hermes update:

  1. Silent git fetch — git fetch ran with capture_output=True, swallowing git's progress output (object enumeration, remote branch listing). Users saw only the literal string "→ Fetching updates..." with no indication anything was happening.

  2. Autostash on every run — git stash push ran BEFORE we knew whether there was an update to apply. On an up-to-date tree with local changes, every hermes update call would:

    • Create a stash named hermes-update-autostash-<timestamp>
    • Trigger an interactive restore prompt ("Restore local changes now? [Y/n]")
    • Restore the stash
    • Exit cleanly

    …for nothing. Clutters the stash list, breaks --yes cron updates, and prompts in non-interactive contexts.

Repro

# On up-to-date main with one local uncommitted change
echo "# local note" >> README.md
hermes update

Expected: update completes silently (or with progress), no stash prompt.
Actual: "⚠ Local changes were stashed before updating." → "Restore local changes now? [Y/n]" prompt → stash restored.

Root Cause

# BEFORE — hermes_cli/main.py:9620 (and 9654)
if current_branch != branch:
    auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)  # ← unconditional
    checkout_result = ...
else:
    auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)  # ← unconditional

# THEN we check for updates...
result = subprocess.run(git_cmd + ["rev-list", f"HEAD..origin/{branch}", "--count"], ...)
commit_count = int(result.stdout.strip())
if commit_count == 0:
    # ... restore the stash we never should have created
# BEFORE — hermes_cli/main.py:9574
fetch_result = subprocess.run(
    git_cmd + ["fetch", "origin", branch],
    cwd=PROJECT_ROOT,
    capture_output=True,  # ← swallows git progress
    text=True,
)

Fix

1. Visible fetch progress

Switch the user-facing fetch to capture_output=False. On failure, re-run with capture_output=True so we can still pattern-match stderr for network/auth errors (the existing Could not resolve host, Authentication failed, etc. logic is preserved).

2. Deferred stash

Move _stash_local_changes_if_needed to AFTER the rev-list count check. If commit_count == 0, we exit cleanly without ever creating a stash. The race condition where someone else pulls between our check and our stash is still handled correctly by the post-stash commit_count == 0 block.

3. Removed redundant rev-list re-run

The original code ran rev-list twice (once before the stash, once after). Since git stash doesn't move HEAD, the second call was redundant. Removed.

Tests Added

Three new tests in tests/hermes_cli/test_cmd_update.py:

  • test_3523_no_stash_when_up_to_date_with_local_changes — asserts git stash push is NOT called when up to date, even with dirty tree
  • test_3523_fetch_stderr_visible_on_success — asserts user-facing fetch has capture_output=False
  • test_3523_stash_still_happens_when_update_available — asserts stash still happens when update IS available (regression guard for the deferred-stash fix)

All 30 tests pass (27 existing + 3 new).

Checklist

  • Reproduces on current main
  • Points to exact line where bug manifests
  • Fixes the whole bug class (sibling call paths included) — both fetch output suppression AND autostash are fixed
  • E2E validation — all 30 tests in test_cmd_update.py pass
  • Behavior contracts (invariant: no stash when count==0) over snapshots
  • Cache-, alternation-, and invariant-safe — no changes to the conversation loop or system prompt
  • Prompt caching preserved — capture_output doesn't affect what gets cached
  • No new HERMES_* env vars
  • No outbound telemetry added

…l update confirmed (NousResearch#3523)

Regression from NousResearch#3492 introduced two user-visible issues in `hermes update`:

1. `git fetch` was run with `capture_output=True`, swallowing git's
   progress output (object enumeration, remote branch listing).
2. `git stash push` ran BEFORE we knew whether there was an update to
   apply. On an up-to-date tree with local changes, every `hermes update`
   call would autostash-and-pop for nothing — cluttering the stash list
   and triggering an interactive restore prompt.

Fix:
- Switch the user-facing fetch to `capture_output=False` so progress
  reaches the terminal. Re-run with `capture_output=True` only on
  failure so we can still pattern-match stderr for network/auth errors.
- Move the `git status` + `git stash push` block to AFTER the
  `git rev-list HEAD..origin/main --count` check. If the count is 0,
  no stash is created and we exit cleanly.
- The race where someone else pulls between our check and our stash
  is still handled by the post-stash `commit_count == 0` block.

Tests:
- `test_3523_no_stash_when_up_to_date_with_local_changes`: verifies no
  `git stash push` is called when up to date.
- `test_3523_fetch_stderr_visible_on_success`: verifies the user-facing
  fetch uses `capture_output=False`.
- `test_3523_stash_still_happens_when_update_available`: verifies the
  fix didn't break the normal update-with-local-changes path.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists labels Jul 12, 2026
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for isolating two real hermes update regressions. The current checkout still captures fetch output at hermes_cli/main.py:9573 and stashes before the count check at hermes_cli/main.py:9650.

Problems

  • The deferred-stash change removes the protection that main intentionally applies before a cross-branch checkout (hermes_cli/main.py:9616). If HEAD differs from the target and a local edit would be overwritten, git checkout <branch> fails before the new post-rev-list stash path can run. The fallback checkout then reports the branch missing even though the local edit is the blocker.

Suggested changes

  • Preserve local changes before a required branch switch, or compute whether the target branch is ahead without checking it out and only then stash before checkout.
  • Add a regression test for dirty, conflicting edits while switching branches; the added tests cover only the same-branch path.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
@teknium1 teknium1 added the area/install-update Installer, updater, packaging, wheels, doctor label Jul 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hermes update regressions after #3492 — silent git output and unnecessary stashes

3 participants